﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace FxArchitect.Xna_3_1
{
    public class GraphicsResourceManager : DrawableGameComponent
    {
        static VertexDeclaration m_vertexDecl;
        static VertexBuffer m_vertexBuffer;
        static IndexBuffer m_indexBuffer;

        Texture2D[] m_textures;
        string[] m_textureToLoad;
        int m_pendingTextureLoads;
        object m_textureLock = new object();

        SamplerStateBlock[] m_psSamplerStates;
        object m_samplerStateLock = new object();

        Effect m_effect;
        byte[] m_effectCode;
        string m_activeTechniqueName;
        object m_effectLock = new object();

        public int SamplerStateCount { get { return m_psSamplerStates.Length; } }
        public string ActiveTechniqueName
        {
            get { lock (m_effectLock) return m_activeTechniqueName; }
            set { lock (m_effectLock) m_activeTechniqueName = value; }
        }
        public byte[] EffectCode
        {
            get { lock (m_effectLock) return m_effectCode; }
            set { lock (m_effectLock) m_effectCode = value; }
        }

        /// <summary>
        /// Raised when a new effect is created, usually called after a successful compilation.
        /// </summary>
        public Action<Effect> EffectCreated = delegate { };
        /// <summary>
        /// Raised before drawing begins to update effect parameters.
        /// </summary>
        public Action<GameTime, Effect> UpdateEffect = delegate { };

        public GraphicsResourceManager(Game game)
            : base(game)
        {
        }

        protected override void LoadContent()
        {
            base.LoadContent();

            int maxTextures = GraphicsDevice.GraphicsDeviceCapabilities.MaxSimultaneousTextures;
            if (m_psSamplerStates == null || m_psSamplerStates.Length != maxTextures)
            {
                int prevCount = 0;
                if (m_psSamplerStates == null)
                {
                    m_psSamplerStates = new SamplerStateBlock[maxTextures];
                    m_textures = new Texture2D[maxTextures];
                    m_textureToLoad = new string[maxTextures];
                }
                else
                {
                    prevCount = m_textureToLoad.Length;
                    Array.Resize<SamplerStateBlock>(ref m_psSamplerStates, maxTextures);
                    Array.Resize<string>(ref m_textureToLoad, maxTextures);
                    Array.Resize<Texture2D>(ref m_textures, maxTextures);
                }

                if (maxTextures > prevCount)
                {
                    for (int i = prevCount; i < m_psSamplerStates.Length; i++)
                    {
                        m_psSamplerStates[i].AddressU = TextureAddressMode.Clamp;
                        m_psSamplerStates[i].AddressV = TextureAddressMode.Clamp;
                        m_psSamplerStates[i].AddressW = TextureAddressMode.Clamp;
                        m_psSamplerStates[i].MagFilter = TextureFilter.Linear;
                        m_psSamplerStates[i].MinFilter = TextureFilter.Linear;
                    }
                }
            }
        }

        void TransferParameter(EffectParameter prev, EffectParameter next)
        {
            if (prev.ParameterClass == EffectParameterClass.Struct &&
                next.ParameterClass == EffectParameterClass.Struct)
            {
                foreach (EffectParameter member in prev.StructureMembers)
                {
                    EffectParameter nextMember = next.StructureMembers[member.Name];
                    if (nextMember != null) TransferParameter(member, nextMember);
                }
            }
            else if ((prev.ParameterClass == EffectParameterClass.Scalar ||
                      prev.ParameterClass == EffectParameterClass.Vector ||
                      prev.ParameterClass == EffectParameterClass.MatrixRows ||
                      prev.ParameterClass == EffectParameterClass.MatrixColumns) &&
                     (next.ParameterClass == EffectParameterClass.Scalar ||
                      next.ParameterClass == EffectParameterClass.Vector ||
                      next.ParameterClass == EffectParameterClass.MatrixRows ||
                      next.ParameterClass == EffectParameterClass.MatrixColumns))
            {
                for (int i = 0; i < prev.Elements.Count && i < next.Elements.Count; i++)
                {
                    if (prev.Elements[i].ParameterClass == EffectParameterClass.Scalar &&
                        next.Elements[i].ParameterClass == EffectParameterClass.Scalar)
                    {
                        if (prev.Elements[i].ParameterType == EffectParameterType.Bool)
                        {
                            bool value = prev.Elements[i].GetValueBoolean();
                            next.Elements[i].SetValue(value);
                        }
                        else if (prev.Elements[i].ParameterType == EffectParameterType.Int32)
                        {
                            int value = prev.Elements[i].GetValueInt32();
                            next.Elements[i].SetValue(value);
                        }
                        else if (prev.Elements[i].ParameterType == EffectParameterType.Single)
                        {
                            float value = prev.Elements[i].GetValueSingle();
                            next.Elements[i].SetValue(value);
                        }
                    }
                    else
                        TransferParameter(prev.Elements[i], next.Elements[i]);
                }
            }
        }

        public override void Draw(GameTime gameTime)
        {
            #region Load Resources

            if (m_vertexDecl == null)
            {

                VertexPositionTexture[] vertices = new VertexPositionTexture[]
                {
                    new VertexPositionTexture(new Vector3(-1, 1, 0.5f), new Vector2(0, 0)),
                    new VertexPositionTexture(new Vector3(1, 1, 0.5f), new Vector2(1, 0)),
                    new VertexPositionTexture(new Vector3(1, -1, 0.5f), new Vector2(1, 1)),
                    new VertexPositionTexture(new Vector3(-1, -1, 0.5f), new Vector2(0, 1))
                };
                ushort[] indices = new ushort[]
                {
                    0, 1, 2,
                    0, 2, 3
                };

                m_vertexDecl = new VertexDeclaration(GraphicsDevice, VertexPositionTexture.VertexElements);

                m_vertexBuffer = new VertexBuffer(GraphicsDevice,
                                                  VertexPositionTexture.SizeInBytes * vertices.Length,
                                                  BufferUsage.WriteOnly);
                m_vertexBuffer.SetData<VertexPositionTexture>(vertices);

                m_indexBuffer = new IndexBuffer(GraphicsDevice,
                                                sizeof(ushort) * indices.Length,
                                                BufferUsage.WriteOnly, IndexElementSize.SixteenBits);
                m_indexBuffer.SetData<ushort>(indices);
            }

            lock (m_effectLock)
            {
                if (m_effectCode != null)
                {
                    Effect newEffect = new Effect(GraphicsDevice, m_effectCode, CompilerOptions.None, null);
                    if (m_effect != null)
                    {
                        //foreach (EffectParameter prev in m_effect.Parameters)
                        //{
                        //    EffectParameter next = newEffect.Parameters[prev.Name];
                        //    if (next != null) TransferParameter(prev, next);
                        //}

                        m_effect.Dispose();
                        m_effect = null;
                    }

                    m_effect = newEffect;
                    m_effectCode = null;
                    EffectCreated(m_effect);
                }
            }

            if (m_pendingTextureLoads > 0)
            {
                lock (m_textureLock)
                {
                    for (int i = 0; i < m_textures.Length; i++)
                    {
                        if (!string.IsNullOrEmpty(m_textureToLoad[i]))
                        {
                            if (m_textures[i] != null)
                            {
                                m_textures[i].Dispose();
                                m_textures[i] = null;
                            }

                            m_textures[i] = Texture2D.FromFile(GraphicsDevice, m_textureToLoad[i]);
                            m_textures[i].Name = m_textureToLoad[i];
                            m_pendingTextureLoads--;
                        }
                        m_textureToLoad[i] = null;
                    }
                }
            }

            #endregion

            #region Apply State

            lock (m_samplerStateLock)
            {
                for (int i = 0; i < m_psSamplerStates.Length; i++)
                    m_psSamplerStates[i].ApplyState(GraphicsDevice, i);
            }

            lock (m_textureLock)
            {
                bool isViewportSet = false;
                for (int i = 0; i < m_textures.Length; i++)
                {
                    if (!isViewportSet && m_textures[i] != null)
                    {
                        Viewport vp = GraphicsDevice.Viewport;
                        vp.Width = m_textures[i].Width;
                        vp.Height = m_textures[i].Height;
                        GraphicsDevice.Viewport = vp;
                        isViewportSet = true;
                    }
                    GraphicsDevice.Textures[i] = m_textures[i];
                }
            }

            GraphicsDevice.VertexDeclaration = m_vertexDecl;
            GraphicsDevice.Vertices[0].SetSource(m_vertexBuffer, 0, VertexPositionTexture.SizeInBytes);
            GraphicsDevice.Indices = m_indexBuffer;

            #endregion

            if (m_effect != null)
            {
                OnUpdateEffect(gameTime);

                try
                {
                    m_effect.Begin();
                    foreach (EffectPass pass in m_effect.CurrentTechnique.Passes)
                    {
                        pass.Begin();
                        GraphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList,
                                                             0, 0, 4,
                                                             0, 2);
                        pass.End();
                    }
                    m_effect.End();
                }
                catch { }
            }

            base.Draw(gameTime);
        }

        void OnUpdateEffect(GameTime gameTime)
        {
            foreach (EffectParameter ep in m_effect.Parameters)
            {
                foreach (EffectAnnotation ea in ep.Annotations)
                {
                    if (!ea.Name.Equals("Source", StringComparison.CurrentCultureIgnoreCase) ||
                        ea.ParameterType != EffectParameterType.String)
                        continue;

                    string v = ea.GetValueString();
                    string[] cp = v.Split('.');
                    if (cp != null && cp.Length > 0)
                    {
                        if (cp[0].Equals("GameTime", StringComparison.CurrentCultureIgnoreCase) &&
                            cp.Length >= 3)
                        {
                            TimeSpan time = TimeSpan.Zero;
                            if (cp[1].Equals("ElapsedGameTime", StringComparison.CurrentCultureIgnoreCase) ||
                                cp[1].Equals("ElapsedTime", StringComparison.CurrentCultureIgnoreCase))
                                time = gameTime.ElapsedGameTime;
                            else if (cp[1].Equals("TotalGameTime", StringComparison.CurrentCultureIgnoreCase) ||
                                     cp[1].Equals("TotalTime", StringComparison.CurrentCultureIgnoreCase))
                                time = gameTime.TotalGameTime;

                            if (cp[2].Equals("Days", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(time.Days);
                            else if (cp[2].Equals("Hours", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(time.Hours);
                            else if (cp[2].Equals("Minutes", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(time.Minutes);
                            else if (cp[2].Equals("Seconds", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(time.Seconds);
                            else if (cp[2].Equals("Milliseconds", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(time.Milliseconds);
                            else if (cp[2].Equals("Ticks", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(time.Ticks);
                            else if (cp[2].Equals("TotalDays", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue((float)time.TotalDays);
                            else if (cp[2].Equals("TotalHours", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue((float)time.TotalHours);
                            else if (cp[2].Equals("TotalMinutes", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue((float)time.TotalMinutes);
                            else if (cp[2].Equals("TotalSeconds", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue((float)time.TotalSeconds);
                            else if (cp[2].Equals("TotalMilliseconds", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue((float)time.TotalMilliseconds);
                            break;
                        }
                        else if (cp[0].Equals("Viewport", StringComparison.CurrentCultureIgnoreCase) &&
                                 cp.Length >= 2)
                        {
                            if (cp[1].Equals("X", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(GraphicsDevice.Viewport.X);
                            else if (cp[1].Equals("Y", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(GraphicsDevice.Viewport.Y);
                            else if (cp[1].Equals("Width", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(GraphicsDevice.Viewport.Width);
                            else if (cp[1].Equals("Height", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(GraphicsDevice.Viewport.Height);
                            else if (cp[1].Equals("MinDepth", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(GraphicsDevice.Viewport.MinDepth);
                            else if (cp[1].Equals("MaxDepth", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(GraphicsDevice.Viewport.MaxDepth);
                            else if (cp[1].Equals("AspectRatio", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(GraphicsDevice.Viewport.AspectRatio);
                            else if (cp[1].Equals("TitleSafeArea", StringComparison.CurrentCultureIgnoreCase))
                                ep.SetValue(new Vector4(GraphicsDevice.Viewport.TitleSafeArea.X,
                                                        GraphicsDevice.Viewport.TitleSafeArea.Y,
                                                        GraphicsDevice.Viewport.TitleSafeArea.Width,
                                                        GraphicsDevice.Viewport.TitleSafeArea.Height));
                            break;
                        }
                        else if (cp[0].StartsWith("Texture", StringComparison.CurrentCultureIgnoreCase) &&
                                 cp.Length >= 2)
                        {
                            int index = 0;
                            if (cp[0].Length > 7)
                                int.TryParse(cp[0].Substring(7, cp[0].Length - 7), out index);

                            if (index < m_psSamplerStates.Length && m_textures[index] != null)
                            {
                                if (cp[1].Equals("Size", StringComparison.CurrentCultureIgnoreCase))
                                    ep.SetValue(new Vector2(m_textures[index].Width, m_textures[index].Height));
                                else if (cp[1].EndsWith("Width", StringComparison.CurrentCultureIgnoreCase))
                                    ep.SetValue(m_textures[index].Width);
                                else if (cp[1].EndsWith("Height", StringComparison.CurrentCultureIgnoreCase))
                                    ep.SetValue(m_textures[index].Height);
                                else if (cp[1].EndsWith("LevelCount", StringComparison.CurrentCultureIgnoreCase))
                                    ep.SetValue(m_textures[index].LevelCount);
                            }
                            break;
                        }
                    }
                }
            }

            UpdateEffect(gameTime, m_effect);
        }

        public void LoadTexture(string filename, int index)
        {
            lock (m_textureLock)
            {
                m_textureToLoad[index] = filename;
                m_pendingTextureLoads++;
            }
        }
        public Texture2D GetTexturePS(int index)
        {
            lock (m_textureLock)
                return m_textures[index];
        }
        public SamplerStateBlock GetSamplerStatePS(int index)
        {
            lock (m_samplerStateLock)
                return m_psSamplerStates[index];
        }
        public void SetSamplerStatePS(SamplerStateBlock samplerState, int index)
        {
            lock (m_samplerStateLock)
                m_psSamplerStates[index] = samplerState;
        }
    }
}
